from urllib.parse import parse_qs, quote, urljoin
import re
import io
import traceback
import http.cookies
import logging
import datetime
import time
from bodhi.utils.datastructures import MultiValueDict
import json

import http.client

from bodhi.utils.http import http_date, cookie_date

http_responses = http.client.responses

def get_host(environ):
    if 'HTTP_HOST' in environ:
        return environ['HTTP_HOST']
    result = environ['SERVER_NAME']
    if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
       in (('https', '443'), ('http', '80')):
        result += ':' + environ['SERVER_PORT']
    return result

def safe_copyfileobj(fsrc, fdst, length=16*1024, size=0): 
    """ 
    A version of shutil.copyfileobj that will not read more than 'size' bytes. 
    This makes it safe from clients sending more than CONTENT_LENGTH bytes of 
    data in the body. 
    """ 
    if not size: 
        return 
    while size > 0: 
        buf = fsrc.read(min(length, size)) 
        if not buf: 
            break 
        fdst.write(buf) 
        size -= len(buf) 

def get_raw_post_data(environ, max_length=0):
    try:
        # CONTENT_LENGTH might be absent if POST doesn't have content at all (lighttpd)
        content_length = int(environ.get('CONTENT_LENGTH', 0))
    except (ValueError, TypeError):
        # If CONTENT_LENGTH was empty string or not an integer, don't
        # error out. We've also seen None passed in here (against all
        # specs, but see ticket #8259), so we handle TypeError as well.
        content_length = 0
    if max_length and content_length > max_length:
        raise HttpError(413, 'maximum limit: ' + str(max_length))

    with io.BytesIO() as buf:
        safe_copyfileobj(environ['wsgi.input'], buf,
                size=content_length)
        return buf.getvalue()


class HttpError(Exception):
    """An exception that will turn into an HTTP error response."""

    def __init__(self, status_code, log_message=''):
        assert status_code >= 400, 'HttpError is meant to indicate an error, ie. status >= 400.'
        self.status_code = status_code
        self.log_message = log_message

    def __str__(self):
        status = http_responses[self.status_code] 
        return "HTTP {0}: {1}".format(self.status_code, 
                       status + ' (' + self.log_message + ')' if self.log_message else status) 


# script name may be different in an url-rewrite environment
# host may need to be got from HTTP_X_FORWARDED_HOST environment variable in some environments.
# encoding can only be utf-8, because parse_qs don't accept encoding paramenter currently
class HttpRequest(object):
    def __init__(self, environ):
        """
        construct the request by the given WSGI environ. it should be safe to 
        call, and the parts that may raise exceptions should be done in parse.
        """
        self.environ = environ

        self.method = environ["REQUEST_METHOD"].upper()
        self.version = "HTTP/1.1"

        self.path = quote(environ.get("SCRIPT_NAME", "") + environ.get("PATH_INFO", ""))

        self.query = environ.get("QUERY_STRING", "")
        self.uri = self.path + '?' + self.query if self.query else self.path

        self.protocol = environ["wsgi.url_scheme"]
        self.remote_ip = environ.get("REMOTE_ADDR", "")
        self.host = get_host(environ)

        self.is_ajax = environ.get('HTTP_X_REQUESTED_WITH') == 'XMLHttpRequest'

    @property
    def is_secure(self):
        return self.protocol == 'https'

    def parse(self):
        self.arguments = MultiValueDict()
        self.form = MultiValueDict()
        self.files = MultiValueDict()

        if len(self.uri) > 2048:
            raise HttpError(414)

        if self.query:
            self.arguments = MultiValueDict(parse_qs(self.query, keep_blank_values=False))
        
        # Parse request body
        if self.method == 'POST':
            content_type = self.environ.get("CONTENT_TYPE", "")
            if content_type.startswith("application/x-www-form-urlencoded"):
                # parse_qs calls unquote internally, which is very slow
                # large input data should use multipart instead
                body = get_raw_post_data(self.environ, 65536) # ~64K/9 ie. 7.1K Chinese chars.
                dict.update(self.arguments, parse_qs(body.decode('latin-1')))
            elif content_type.startswith("multipart/form-data"):
                # todo: change to a better multipart parser
                body = get_raw_post_data(self.environ, 2048*1024) 
                if 'boundary=' in content_type:
                    boundary = content_type.split('boundary=', 1)[1]
                    if boundary: 
                        from .multipart import parse_multipart
                        form, files = parse_multipart(body, boundary)
                        dict.update(self.arguments, form)
                        self.files = MultiValueDict(files)
                else:
                    logging.warning("Invalid multipart/form-data")

    @property
    def cookies(self):
        """A dictionary of Cookie.Morsel objects."""
        if not hasattr(self, "_cookies"):
            self._cookies = http.cookies.SimpleCookie()
            try:
                self._cookies.load(self.environ.get('HTTP_COOKIE', ''))
            except:
                pass
        return self._cookies

    def get_cookie(self, name, default=None):
        """Gets the value of the cookie with the given name, else default."""
        cookie = self.cookies.get(name)
        return cookie.value if cookie else default

    @property
    def full_uri(self):
        """Reconstructs the full URI for this request."""
        return '{0}://{1}{2}'.format(self.protocol, self.host, self.uri)

    def build_absolute_uri(self, location=None):
        """
        Builds an absolute URI from the location and this request. If no 
        location is specified, returns self.full_uri.

        make sure location is url safe.
        """
        if not location:
            return '{0}://{1}{2}'.format(self.protocol, self.host, self.path)
        assert '://' not in location
        if location.startswith('/'):
            return '{0}://{1}{2}'.format(self.protocol, self.host, location)
        else:
            # urljoin does not do quote or unquote.
            return urljoin(self.full_uri, location) 

    def close(self):
        for files in self.files.lists():
            for f in files:
                try:
                    f['input'].close()
                except OSError as e:
                    if e.errno != 2:
                        # 2: Means the file was moved or deleted before the tempfile could unlink it.
                        raise


from wsgiref.headers import Headers

class HttpResponse:
    status_code = 200
    encoding = 'utf-8'

    def __init__(self, content=b'', status_code=None, content_type=None):
        if status_code:
            assert isinstance(status_code, int), 'status_code should be int.'
            self.status_code = status_code

        # http://www.w3.org/International/O-HTTP-charset
        # Documents transmitted with HTTP that are of type text, such as 
        # text/html, text/plain, etc., can send a charset parameter in the HTTP
        # header to specify the character encoding of the document. 

        # however, here should not add "; charset=utf-8" automatically, 
        # because as of image, charset is meaningless.

        self.content = content

        content_type = content_type or 'text/html; charset=' + self.encoding
        self.headers = Headers([('Content-Type', content_type)])

        self.cookies = http.cookies.SimpleCookie()

    def __str__(self):
        """Full HTTP message, including headers."""
        return str(self.headers)

    def has_header(self, name):
        return name in self.headers

    def get_header(self, name):
        return self.headers.get(name)

    def set_header(self, name, value):
        """Sets the given response header name and value.

        If a datetime is given, we automatically format it according to the
        HTTP specification. If the value is not a string, we convert it to
        a string. All header values are then encoded as UTF-8.
        """
        if isinstance(value, datetime.datetime):
            value = http_date(value)
        elif isinstance(value, int):
            value = str(value)
        self.headers[name] = value

    def set_cookie(self, name, value, max_age=None, path="/", domain=None, secure=False, httponly=False):
        self.cookies[name] = value
        if domain is not None:
            self.cookies[name]["domain"] = domain
        if max_age is not None:
            # IE6,7,8 does not support max-age, all browsers support expires
            self.cookies[name]["expires"] = cookie_date(time.time() + max_age)
        if path is not None:
            self.cookies[name]["path"] = path
        if secure:
            self.cookies[name]['secure'] = True
        if httponly:
            self.cookies[name]['httponly'] = True

    def delete_cookie(self, name, path="/", domain=None):
        """Deletes the cookie with the given name."""
        self.set_cookie(name, value="", path=path, max_age=-300000000, domain=domain)

    def write(self, s):
        if s:
            if isinstance(s, str):
                s = s.encode(self.encoding)
            self._write_buffer.append(s)

    def json(self, obj):
        self.content_type = 'application/json'
        self.content = json.dumps(obj)

    @property
    def content(self):
        return b''.join(self._write_buffer)

    @content.setter
    def content(self, s):
        self._write_buffer = []
        self.write(s)

    def clear(self):
        self._write_buffer = []
    
    def __iter__(self):
        for s in self._write_buffer:
            yield s

    @property
    def content_type(self):
        return self.headers['Content-Type']

    @content_type.setter
    def content_type(self, type):
        self.headers['Content-Type'] = type

    def redirect(self, uri, permanent=False):
        self.clear()
        self.status_code = 301 if permanent else 302
        #uri = quote(redirect_to.encode(self.encoding), safe="/#%[]=:;$&()+,!?*@'~")
        self.set_header('Location', uri)

